package bueu.bexl;


public class Parser {

	private final char[] chars;
	private final int length;
	private int index = 0;
	private char close = 0;
	
	private Parser(String expr) {
		this.chars = expr.toCharArray();
		this.length = this.chars.length;
	}

	public static final BexlExpr parse(final String expr) throws ParseBexlException {
		return new Parser(expr).parse(10, (char) 0, false, false);
	}

	private final BexlExpr parse(final int order, final char open, final boolean nullable, final boolean function) throws ParseBexlException {
		BexlExpr expr = null;

		for (; this.index < this.length;) {
			if (this.close != 0) {
				if (expr == null) throw new ParseBexlException();
				
				return expr;
			}

			char ch = this.chars[this.index++];
		
			switch (ch) {
			case ' ' : case '\t' : // Spaces
				break;
			case '-' :
				if (expr == null) {
					expr = new MinusExpr(parse(1, open, false, false));
				} else if (order > 3) {
					expr = new SustractExpr(expr, parse(3, open, false, false)); 
				} else {
					this.index--;
					return expr;
				}
				
				break;
			case '!' :
				if (this.index >= this.length) throw new ParseBexlException();

				if (this.chars[this.index] == '=') {
					if (expr == null) throw new ParseBexlException();
					
					if (order > 4) {
						this.index++;
						expr = new NEqExpr(expr, parse(4, open, false, false));
					} else {
						this.index--;
						return expr;
					}
				} else {
					if (expr != null) throw new ParseBexlException();

					expr = new NotExpr(parse(1, open, false, false));
				}
				
				break;
			case '+' : 
				if (expr == null) throw new ParseBexlException();

				if (order > 3) {
					expr = new AddExpr(expr, parse(3, open, false, false)); 
				} else {
					this.index--;
					return expr;
				}

				break;
			case '*' : case '/' : case '%' :
				if (expr == null) throw new ParseBexlException();
				
				if (order > 2) {
					switch (ch) {
					case '*' : expr = new MultiplyExpr(expr, parse(2, open, false, false)); break;
					case '/' : expr = new DivExpr(expr, parse(2, open, false, false)); break;
					case '%' : expr = new ModExpr(expr, parse(2, open, false, false)); break;
					}
				} else {
					this.index--;
					return expr;
				}
				
				break;
			case '=' :
				if (expr == null || this.index >= this.length || this.chars[this.index++] != '=') throw new ParseBexlException();
					
				if (order > 4) {
					expr = new EqExpr(expr, parse(4, open, false, false));
				} else {
					this.index -= 2;
					return expr;
				}
				
				break;
			case '<' : case '>' :
				if (expr == null || this.index >= this.length) throw new ParseBexlException();

				if (order > 4) {
					if (this.chars[this.index] == '=') {
						this.index++;
						expr = ch == '<'? new LeExpr(expr, parse(4, open, false, false)) : new GeExpr(expr, parse(4, open, false, false));
					} else {
						expr = ch == '<'? new LtExpr(expr, parse(4, open, false, false)) : new GtExpr(expr, parse(4, open, false, false));
					}
				} else {
					this.index--;
					return expr;
				}
				
				break;
			case '&' : case '|' :
				if (expr == null || this.index >= this.length || this.chars[this.index++] != ch) throw new ParseBexlException();
					
				if (order > 5) {
					expr = ch == '&'? new AndExpr(expr, parse(5, open, false, false)) : new OrExpr(expr, parse(5, open, false, false));
				} else {
					this.index -= 2;
					return expr;
				}
				
				break;
			case '[' : {
				if (expr != null) throw new ParseBexlException();

				BexlExpr filter = parse(10, ']', false, false);

				if (this.close == ',') {
					expr = parseInterval(filter, true);
				} else {
					validateClose(']');
					BexlExpr next = null;
					
					if (this.index < this.length) {
						char ch2 = this.chars[this.index];
	
						if (ch2 == '.' || ch2 == '[') {
							next = parse(0, open, true, false);
						}
					}
	
					expr = new FilterExpr(filter, next);
				}

				break;
			}
			case '{' : {
				if (expr != null) throw new ParseBexlException();

				if (parseSpacesTo('}')) {
					expr = new ArrayExpr(null);
				} else { 
					expr = new ArrayExpr(parse(10, '}', false, false));
					validateClose('}');
				}

				break;
			}
			case '(' : {
				if (expr != null) throw new ParseBexlException();
				
				expr = parse(10, ')', false, false);
				
				if (this.close == ',') {
					expr = parseInterval(expr, false);
				} else {
					validateClose(')');
				}

				break;
			}
			case ')' : case ']' : case '}' : 
				if (expr == null || (open != 'I' && ch != open)
						|| (open == 'I' && ch != ')' && ch != ']')) throw new ParseBexlException();

				this.close = ch;

				return expr;
			case '0' : case '1' : case '2' : case '3' : case '4' :
			case '5' : case '6' : case '7' : case '8' : case '9' : {
				if (expr != null) throw new ParseBexlException();

				final int ini = this.index - 1;
				int i = ini;
				boolean punto = false;

				for (; i < this.length; i++) {
					char ch2 = this.chars[i];

					if (ch2 == '.') {
						if (punto) throw new ParseBexlException();

						punto = true;
					} else if (ch2 < 48 || ch2 > 57) {
						break;
					}
				}

				expr = new ValueExpr(new Double(new String(this.chars, ini, i - ini)));
				this.index = i;

				break;
			}
			case '\'' : {
				if (expr != null) throw new ParseBexlException();

				final int ini = this.index;
				int i = ini;

				for (; i < this.length; i++) {
					if (this.chars[i] == '\'') {
						expr = new ValueExpr(new String(this.chars, ini, i - ini));
						break;
					}
				}

				if (this.chars[i] != '\'') throw new ParseBexlException();
				
				this.index = i + 1;

				break;
			}
			case '.' : {
				if (!nullable) throw new ParseBexlException("No es property");

				final String name = parseName();
				BexlExpr next = null;

				if (name == null) throw new ParseBexlException();
				
				if (this.index < this.length) {
					char ch2 = this.chars[this.index];

					if (ch2 == '.' || ch2 == '[') {
						next = parse(0, open, true, false);
					}
				}

				expr = new PropertyExpr(name, false, next);

				break;
			}
			case '$' : {
				if (expr != null) throw new ParseBexlException();

				final String name = parseName();
				BexlExpr next = null;

				if (name == null) throw new ParseBexlException();
				
				if (this.index < this.length) {
					char ch2 = this.chars[this.index];

					if (ch2 == '.' || ch2 == '[') {
						next = parse(0, open, true, false);
					}
				}

				expr = new PropertyExpr(name, true, next);
				break;
			}
			case ',' : {
				if (expr == null || function && open != ')') throw new ParseBexlException();

				if (!function && (open == ')' || open == ']')) {
					this.close = ',';
					return expr;
				}

				expr = new ParamExpr(expr, parse(10, open, false, function));

				break;
			}
			case ':' : {
				if (expr == null) throw new ParseBexlException();

				this.close = ch;

				return expr;
			}
			case '?' : {
				if (expr == null) throw new ParseBexlException();
				
				if (order > 9) {
					BexlExpr then = parse(10, open, false, false);
					validateClose(':');
					expr = new ConditionalExpr(expr, then, parse(10, open, false, false));
				} else {
					this.index--;
					return expr;
				}

				break;
			}
			default : // Function or property
			{
				this.index--;
				final String name = parseName();

				if (name == null) throw new ParseBexlException();

				if (name.equals("in")) {
					if (expr == null) throw new ParseBexlException();

					if (order > 6) {
						expr = new InExpr(expr, parse(6, open, false, false)); 
					} else {
						this.index--;
						return expr;
					}

					break;
				}
				
				if (expr != null) throw new ParseBexlException();

				if (name.equals("null")) {
					expr = new ValueExpr(null);
				} else if (name.equals("true")) {
					expr = new ValueExpr(true);
				} else if (name.equals("false")) {
					expr = new ValueExpr(false);
				} else {
					boolean isFunction = parseSpacesTo('(');
	
					if (isFunction) {
						if (parseSpacesTo(')')) {
							expr = new FunctionExpr(name, null);
						} else {
							expr = new FunctionExpr(name, parse(10, ')', false, true));
							validateClose(')');
						}
					} else {
						BexlExpr next = null;
						
						if (this.index < this.length) {
							char ch2 = this.chars[this.index];

							if (ch2 == '.' || ch2 == '[') {
								next = parse(0, open, true, false);
							}
						}

						expr = new PropertyExpr(name, false, next);
					}
				}
			}
			}
		}
		
		if (!nullable && expr == null) throw new ParseBexlException();

		return expr;
	}
	
	private final void validateClose(char closeChar) {
		if (this.close != closeChar) throw new ParseBexlException();

		this.close = 0;
	}
	
	private final BexlExpr parseInterval(final BexlExpr lower, final boolean lowerClosed) {
		this.close = 0;
		final BexlExpr upper = parse(10, 'I', false, false);
		
		if (upper instanceof ParamExpr) throw new ParseBexlException();
		
		if (this.close != ')' && this.close != ']') throw new ParseBexlException();
		
		final boolean upperClosed = this.close == ']';
		this.close = 0;
		
		return new IntervalExpr(lower, lowerClosed, upper, upperClosed);
	}
	
	private final boolean parseSpacesTo(final char ch) {
		int i = this.index;

		for (; i < this.length; i++) {
			char c = this.chars[i];

			switch(c) {
			case ' ' : case '\t' : continue;
			case '(' : case ')' : case '}' :
				if (c == ch) {
					this.index = i + 1;
					return true;
				}
			}

			break;
		}
		
		return false;
	}

	private final String parseName() {
		final int length = this.chars.length;
		final int ini = this.index;
		int i = 0;
	
		for (int j = ini; j < length; j++) {
			char ch = this.chars[j];
	
			if ((ch >= 48 && ch <= 57) || (ch >= 65 && ch <= 90)
					|| (ch >= 97 && ch <= 122) || ch == 95) {
				i++;
			} else {
				break;
			}
		}
		
		this.index += i;

		return i > 0? new String(this.chars, ini, i) : null;
	}
}
